/**
 * 
 */
package com.intelap.fabricaciones;

import static java.lang.annotation.ElementType.FIELD;
import static java.lang.annotation.ElementType.METHOD;
import static java.lang.annotation.ElementType.PARAMETER;
import static java.lang.annotation.ElementType.TYPE;
import static java.lang.annotation.RetentionPolicy.RUNTIME;

import java.io.Serializable;
import java.lang.annotation.Documented;
import java.lang.annotation.Retention;
import java.lang.annotation.Target;
import java.util.logging.Logger;

import javax.annotation.PostConstruct;
import javax.annotation.PreDestroy;
import javax.annotation.Resource;
import javax.ejb.AfterCompletion;
import javax.ejb.ConcurrentAccessTimeoutException;
import javax.ejb.Remove;
import javax.ejb.Stateful;
import javax.ejb.TransactionAttribute;
import javax.ejb.TransactionAttributeType;
import javax.enterprise.context.Conversation;
import javax.enterprise.context.ConversationScoped;
import javax.enterprise.context.Dependent;
import javax.inject.Inject;
import javax.inject.Qualifier;
import javax.persistence.EntityManager;
import javax.persistence.EntityManagerFactory;
import javax.persistence.FlushModeType;
import javax.persistence.PersistenceContext;
import javax.persistence.PersistenceContextType;
import javax.persistence.PersistenceUnitUtil;
import javax.transaction.Status;
import javax.transaction.TransactionSynchronizationRegistry;


/** UnitOfWork implementado con el persistence context ofrecido por JPA. Para brindar mas control sobre el ciclo de vida del contexto de persistencia se lo trata en modo "application-managed" asociado a un SFSB.
 * @author Gustavo */
@Stateful
//@Dependent
//@UnidadTrabajo.Nueva
@ConversationScoped
public class UnidadTrabajo implements Serializable {
//	@PersistenceUnit
//	private EntityManagerFactory emf;
	@Inject
	private Conversation conversacion;
	@Resource
	private TransactionSynchronizationRegistry txReg;

	/** Este {@link EntityManager} es el que maneja el contexto de persistencia donde estan todas las entidades consideradas por esta {@link UnidadTrabajo}.
	 * O sea, que toda entidad que se pretenda sea considerada por esta {@link UnidadTrabajo} debe ser cargada usando este {@link EntityManager}. */
	@PersistenceContext(type=PersistenceContextType.EXTENDED)
	private EntityManager em;

	@PostConstruct
	void postConstruct() {
		logger.info("postConstruct: unidadTrabajo#" + System.identityHashCode(this));
//		this.em = this.emf.createEntityManager();
		this.em.setFlushMode(FlushModeType.COMMIT);
	}

	/**Da {@code true} si el contexto de persistencia de esta {@link UnidadTrabajo} contiene a la entidad recibida. */
	@TransactionAttribute(TransactionAttributeType.SUPPORTS)
	public boolean contiene(Object entidad) {
		return this.em.contains(entidad);
	}

	/** Programa el agregado de la 'entidad' recibida en el contexto de persistencia de esta {@link UnidadTrabajo}. */
	@TransactionAttribute(TransactionAttributeType.SUPPORTS)
	public void agregar(Object entidad) {
		this.em.persist(entidad);
	}

	/** Programa el quitado de la 'entidad' recibida en el contexto de persistencia de esta {@link UnidadTrabajo}. */
	@TransactionAttribute(TransactionAttributeType.SUPPORTS)
	public void quitar(Object entidad) {
		this.em.remove(entidad);
	}

	/**La transaccion que este activa cuando esta operacion es llamada trasladará, cuando sea cometida, todos los cambios (entidades agregadas, quitadas, y modificadas) en el contexto de persistencia de esta {@link UnidadTrabajo} que fueron realizados desde el último traslado.    
	 * Los cambios en el contexto de persistencia de esta {@link UnidadTrabajo} NO son trasladados a la base de datos subyacente como parte de la transaccion activa a no ser que esta operación sea llamada. Debe ser llamada mientras hay una transaccion activa, y los cambios se trasladarán a la base de datos subyacente en algún momento entre la llamada esta operacion y el cometido de la transaccion.  */
	@TransactionAttribute(TransactionAttributeType.MANDATORY)
	public void unirseATransaccion() {
//		em.joinTransaction();
	}
	
	/**Traslada inmediatamente, hacia la base de datos subyacente, todos los cambios (entidades agregadas, quitadas, y modificadas) en el contexto de persistencia de esta {@link UnidadTrabajo} que fueron realizados desde el último traslado.<br/>
	 * Si hay una transacción activa cuando esta operación es llamada, los cambios trasladados serán parte de ella. Si tal transaccion fuera cancelada luego, los cambios no se registrarian.
	 * Esta operación solo es necesaria cuando NO se quiere esperar a que la implementacion del contenedor de persistencia decida hacer el traslado, que a mas tardar será cuando la transaccion activa sea cometida.*/
	@TransactionAttribute(TransactionAttributeType.REQUIRED)
	public void forzarTrasladoCambios() {
//		this.em.joinTransaction();
		this.em.flush();
	}

	/**Aborta todos los cambios en el contexto de persistencia de esta {@link UnidadTrabajo} y, si la {@link Conversation} esta en modo "long-running" la pasa a modo "transient" para que éste SFMB termine su ciclo de vida al final del request.
	 * Ademas, si hay una transaccion activa la marca como solo desechable (rollback only). */
	@TransactionAttribute(TransactionAttributeType.SUPPORTS)
	public void abortar() {
		this.em.clear();
		if (!conversacion.isTransient())
			conversacion.end();
		if( this.txReg.getTransactionStatus() == Status.STATUS_ACTIVE)
			this.txReg.setRollbackOnly();
	}

	/**Convierte a esta {@link UnidadTrabajo} en inutilizable. Todos los cambios en el contexto de persistencia de esta {@link UnidadTrabajo} que no hayan sido trasladados hacia la base de datos suyacente son ignorados. */
	@Remove
	@TransactionAttribute(TransactionAttributeType.SUPPORTS)
	public void inutilizar() {
		logger.info("inutilizar: unidadTrabajo#" + System.identityHashCode(this));
		if( this.em.isOpen()) {
			this.abortar();
//			this.em.close();
		}
	}
	
	/**Si el contexto de persistencia de esta {@link UnidadTrabajo} contiene a la entidad recibida, da lo mismo que recibe.<br>
	 * Sino, da la entidad cuyo tipo e identidad es igual al de la entidad desatada recibida. Si el contexto de persistencia no la tiene cargada, la carga. */
	@SuppressWarnings("unchecked")
	public <E> E getInstanciaAtada(E entidad) {
//		PersistenceUnitUtil puu = this.emf.getPersistenceUnitUtil();
		PersistenceUnitUtil puu = this.em.getEntityManagerFactory().getPersistenceUnitUtil();
		return this.em.find((Class<? extends E>)entidad.getClass(), puu.getIdentifier(entidad));
	}

	/** Todas las entidades que se pretenda sean consideradas por esta {@link UnidadTrabajo} deben ser cargadas con, o registradas en, el contexto de persistencia asociado a {@link #em}.
	 * Pero como la clase {@link EntityManager} no es thread safe sería imprudente exponerlo con un getter (a {@link #em}). Entonces, para impedir problemas de concurrencia, toda operacion que necesite el {@link EntityManager} debe ser embutida en una {@link Operacion} y ejecutada mediante esta operacion.
	 * De esta manera tendrá acceso al {@link #em} y se ejecutará dentro de este session-bean bussness method con todas las ventajas y desventajas que esto implica.
	 * ADVERTENCIA: Para evitar deadlocks, se recomienda evitar llamar session-beans' bussness-methods desde la {@link Operacion} recibida. */
	@TransactionAttribute(TransactionAttributeType.REQUIRED)
	public <R, E extends Throwable> R ejecutar(Operacion<R, E> operacion) throws E {
//		this.em.joinTransaction();
		return operacion.ejecutarme(this, this.em);
	}

	public interface Operacion<R, E extends Throwable> {
		/** @param ut referencia directa (en opocision a proxiada) de la {@link UnidadTrabajo} que ejecutó la operación {@link UnidadTrabajo#ejecutar(Operacion)} recibiendo a esta {@link Operacion}. Esta referencia es necesaria para poder usar la {@link UnidadTrabajo} desde la implementacion de esta operacion sin provocar un deadlock o un {@link ConcurrentAccessTimeoutException}. Recordar que los session-bean's bussness-methods bloquean los intentos de reentrar a la operación, incluso desde el mismo hilo de ejecucion.
		 * @param em {@link EntityManager} asociado al contexto de persistencia donde deben ser cargadas/registradas las entidades para que sean consideradas por la {@link UnidadTrabajo} recibida. */
		R ejecutarme(UnidadTrabajo ut, EntityManager em) throws E;
	}

	
	/** Da la {@link EntityManagerFactory} asociada al contexto de persistencia correspondiente a esta {@link UnidadTrabajo}. */
	@TransactionAttribute(TransactionAttributeType.SUPPORTS)
	public EntityManagerFactory getEntityManagerFactory() {
//		return this.emf;
		return this.em.getEntityManagerFactory();
	}
	
	/**El context scope de {@link UnidadTrabajo} es {@link Dependent} para que el cliente pueda elegir el contexto donde vive la instancia de {@link UnidadTrabajo}. Por ejemplo, si se quisiera que viva en el contexto de "conversacion" bastaría con inyectar una instancia de {@link UnidadTrabajo} en un campo de un {@link ConversationScoped} managed bean.
	 * Dado que el uso típido de {@link UnidadTrabajo} implica el acceso a una misma instancia UT por varios objetos (los que quieren compartir un contexto de persistencia) el managed bean dueño de la instancia UT suele exponer a UT mediante un producer method. Para que ese producer no requiera estar calificado y evitar la necesidad de tener que calificar todos los puntos donde se inyecte la instancia UT, se califica a la clase {@link UnidadTrabajo} con este calificador para evitar ambiguedad.
	 * @deprecated cuando se decidió usar siempre {@link ConversationScoped} para contener la instancia de {@link UnidadTrabajo}. Al menos hasta que cree el nuevo contexto "EnmarcadoPorUnidadTrabajo" */
	@Qualifier
	@Target({ TYPE, METHOD, PARAMETER, FIELD })
	@Retention(RUNTIME)
	@Documented
	public @interface Nueva {}
	
	
	//// Debug ////
	@Inject
	private Logger logger;
	@AfterCompletion
	void afterCompletion(boolean cometido) {
		logger.info("afterCompletion: unidadTrabajo#" + System.identityHashCode(this) + ", cometido=" + cometido);
	}
	@PreDestroy
	void preDestroy() {
		logger.info("preDestroy UnidadTrabajo#"+ System.identityHashCode(this)+", em.isOpen()="+em.isOpen());
//		if( this.em.isOpen())
//			this.em.close();
	}
}
